import gevent
import time
import json
import ast
import pandas as pd
from binance.spot import Spot

from contract_market.strategy.image.type import Action
from contract_market.utils.xt_future import XT
from contract_market.utils.utils import get_uuid # NOQA

from contract_market.initialize import config  # NOQA
from contract_market.initialize.config import *  # NOQA
from contract_market.core.actor import *  # NOQA
from contract_market.core.core import *  # NOQA
from contract_market.server.engine import EventEngine  # NOQA
from contract_market.common.common import EventSystemEngine  # NOQA
from contract_market.common.constant import *  # NOQA
from contract_market.common.rpc.rpc import RPCClient, RPC  # NOQA
from contract_market.base.event import Event , EventData # NOQA
from contract_market.utils.redisdb import redisdb
from contract_market.strategy.image.runtime_params import redis_optimal_state

timeout = 0.2


class XTAPI():

    def __init__(self) -> None:
        self.xt = None
        self.event_data = EventData()
        self.min_qty = None
        self.price_prec = None
        self.price_step = None
        self.quantity_prec = None
        self.contract_size = None
    
    def init_engine(self, event, engine):
        self.event, self.engine = event, engine
        
    def init_client(self, symbol, transcation_type):
        future_address = um_future_address if transcation_type == 'um' else cm_future_address
        print('future_address', future_address)
        self.xt = XT(
            url=future_address,
            access_key=keys.get(symbol)[0],
            secret_key=keys.get(symbol)[1])
        return True

    def init_account(self, symbol):
        res = self.xt.set_auto_margin(symbol=symbol, position_side='LONG')
        res = self.xt.set_auto_margin(symbol=symbol, position_side='SHORT')
        #  
        res = self.xt.set_account_leverage(leverage=1, position_side="LONG", symbol=symbol)  # invalid position side
        res = self.xt.set_account_leverage(leverage=1, position_side="SHORT", symbol=symbol)
        # 
        # res = self.xt.close_all_position()
        # res = self.merge_position(symbol)
        return True

    def init_config(self, symbol):
        config = self.xt.get_market_config(symbol=symbol)
        config = config.get('result')
        self.min_qty = float(config.get('minQty'))
        self.price_prec = int(config.get('pricePrecision'))
        self.price_step = float(config.get('minStepPrice'))
        self.quantity_prec = int(config.get('quantityPrecision'))
        self.contract_size = float(config.get('contractSize'))
        self.quantity_step = 0
        print('market config(p-q-c)', self.price_prec, self.price_step, self.quantity_prec, self.contract_size)
        time.sleep(3)
        return True

    # 
    def precision(self, price: float, type='price'):
        prec = self.price_prec if type == 'price' else self.quantity_prec
        step = self.price_step if type == 'price' else self.quantity_step
        if type == 'price':
            step_prec = len(str(step).split('.')[-1])
            prec = step_prec if prec>step_prec else prec
        if prec == 0:
            return int(price)
        price = str(price)
        # prec - drop
        if '.' in price:
            i, f = price.split('.')
        else:
            i, f = price, '0'
        tp = float(i+'.'+f[:prec])
        # step
        ss = str(step).split('.')[-1]
        ds = int(ss)
        if ds == 0:
            if int(tp)/step == int(tp)//step:
                tp = int(tp)
            else:
                tp = (int(tp)//step+1)*step
            return tp
        # python 精度真垃圾
        if int((tp-int(tp))*10**len(ss))/ds == int((tp-int(tp))*10**len(ss))//ds:
            return tp
        tp = int(float(price)) if len(ss)-1 == 0 else round(float(price), len(ss)-1)
        return tp
    
    def index_price_api(self, symbol):
        price = self.xt.get_index_price(symbol=symbol, size=1)
        price = float(price.get('result').get('p'))
        return price

    def mark_price_api(self, symbol):
        price = self.xt.get_mark_price(symbol=symbol, size=1)
        price = float(price.get('result').get('p'))
        return price
        
    def index_price(self, symbol, method='redis', type='mark_price'):
        '''
        return
            float
        '''
        if method == 'redis':
            price = redisdb.standalone_operator('get', symbol+f':{type}')
            price = float(price)
        elif method == 'ws':
            # NOTE: unsupport mark price
            queue = self.engine.on_index_price(symbol)
            res = queue.get()
            if not res:
                return False
            data = json.loads(res.get('data'))
            price = float(data.get('data').get('p'))
        elif method == 'api':
            if type == 'mark_price':
                price = self.mark_price_api(symbol)
            else:
                price = self.index_price_api(symbol)
        else:
            raise NotImplementedError
        return self.precision(price)

    def balance_api(self, unit, kind='availableBalance'):
        balance = self.xt.get_coin_balance(unit)
        balance = float(balance.get('result').get(kind))
        return balance

    def balance(self, unit, method='redis', kind='availableBalance'):
        '''
        single coin
        return
            float
        '''
        if method == 'redis':
            balance = redisdb.standalone_operator('get', self.xt._XT__access_key+':balance')
            balance = float(balance)
        elif method == 'ws':
            queue = self.engine.on_user_balance(self.xt._XT__access_key)
            res = queue.get()
            if not res:
                return False
            data = json.loads(res.get('data'))
            balance = float(data.get('availableBalance'))
        elif method == 'api':
            balance = self.balance_api(unit=unit, kind=kind)
        else:
            raise NotImplementedError
        return balance

    def merge_position(self, symbol):
        res = self.xt.set_merge_position(symbol=symbol)
        return res

    def position(self, symbol, method='redis'):
        if method == 'redis':
            pos_redis = redisdb.standalone_operator('get', symbol+':user_position')
            # print('pos_redis', pos_redis)
            position = ast.literal_eval(pos_redis.decode())
        else:
            position = self.xt.get_position(symbol=symbol)
            position = position.get('result')
        return position
    
    def open_orders_single_api(self, symbol, page=1):
        '''
        return 
            price, quantity: [float], [float]
        '''
        order = self.xt.get_account_order(symbol=symbol, state="UNFINISHED", page=page, size=100)
        if order and order.get('result') and order.get('result').get('items'):
            order = order.get('result').get('items')
        else:
            order = []
        return order

    def open_orders_api(self, symbol, page=5):
        '''
        return 
            price, quantity: [float], [float]
        '''
        orderbook = pd.DataFrame()
        for i in range(1, page):
            order = self.xt.get_account_order(symbol=symbol, state="UNFINISHED", page=i, size=100)
            if order and order.get('result') and order.get('result').get('items'):
                order = order.get('result').get('items')
                orderbook = pd.concat([orderbook, pd.DataFrame(order)])
            else:
                break
        # jobs = [gevent.spawn(xt_api.cancel, **{'symbol': self.symbol, 'action': act}) for act in action]
        # gevent.joinall(jobs, timeout=1)
        return orderbook
        
    def oorder_side(self, orderbook):
        '''
        '''
        maker = pd.DataFrame(columns=['price', 'quantity', 'side', 'id'])
        if not orderbook.empty:
            maker['price'] = orderbook.price.astype(float)
            maker['quantity'] = orderbook.origQty.astype(float)
            maker['side'] = orderbook.orderSide.apply(lambda x: Action.long if x == Action.long.value else Action.short)
            maker['id'] = orderbook.orderId.astype(int)
        return maker

    def open_orders_sf(self):
        order = [
            {"id": 129686790855502464, "market": "btc_usdt"},
        ]
        data = {
            "xtfuture": {
                '_extra':{
                    "app_name": "test",
                    "user_name": "zhangsan",
                    "note": "---",
                },
                "data": order,
                "accesskey": "c15c7459-b65a-43af-8165-7ac0ae853154",
                "secretkey": "939a9ccc101f4df7075a1d0cdc2d3f79e88b00bd"},
            "uuid": get_uuid(),
            'timeout': 10,
        }
        data = json.dumps(data)
        event_data = EventData()
        event_data.data = data
        e = Event(XTFUTURE_CANCEL_ORDER_EVENT, event_data)
        self.engine.send(e)

    def open_orders(self, symbol, method='redis', page=5):
        if method == 'api':
            oorder = self.open_orders_api(symbol, page=page)
            maker = self.oorder_side(oorder)
        elif method == 'redis':
            t0 = time.time()
            keys = [f'{symbol}:orders'+str(i) for i in range(1, page+1)]
            orders = redisdb.standalone_operator('mget', keys)
            t1 = time.time()

            # timing: 0.2
            # def to_df(data):
            #     if not data:
            #         return pd.DataFrame()
            #     return pd.DataFrame(json.loads(data))
            # # to_df = lambda x: pd.DataFrame(json.loads(x))
            # orderbook = pd.concat(map(to_df, orders))
            
            # timing: 0.1
            orderbook = []
            for order in orders:
                if not order:
                    return []
                orderbook += json.loads(order)
            orderbook = pd.DataFrame(orderbook)
            if not orderbook.empty:
                orderbook['id'] = orderbook['orderId']

            t2 = time.time()
            maker = self.oorder_side(orderbook)
            t3 = time.time()
            maker = maker.drop_duplicates()
            t4 = time.time()
            print('open_orders timing', t1-t0, t2-t1, t3-t2, t4-t3)
        elif method == 'ws':
            maker_redis = redisdb.standalone_operator('get', symbol+':orderbook')
            maker_redis = pd.DataFrame(json.loads(maker_redis))
            if not maker_redis.empty:
                maker_redis['id'] = maker_redis['orderId']
                maker = self.oorder_side(maker_redis)
            else:
                maker = pd.DataFrame()
        else:
            raise NotImplementedError
        if not maker.empty:
            print('open_orders:', len(maker), len(maker[maker.side==Action.long]), len(maker[maker.side==Action.short]))
        return maker

    def send(self, symbol, action, method='api', timeInForce='GTC', mode='open'):
        '''
        quantity is positive/negtive
        
        下单数量单位是张, btc_usdt面值(contractSize)是0.0001, 1张=0.0001btc

        买入开多:"orderSide": "BUY", "positionSide": "LONG"
        卖出开空:"orderSide": "SELL", "positionSide": "SHORT"
        买入平空:"orderSide": "BUY", "positionSide": "SHORT"
        卖出平多:"orderSide": "SELL", "positionSide": "LONG"

        params
            action: {'type', 'price', 'quantity', 'side', 'id'}
        '''
        if not action:
            return False
        if action.get('quantity') <= 0:
            # TODO: unsupport reduce quantity
            return -1
        action['price'] = self.precision(action.get('price'))
        action['quantity'] = self.precision(action.get('quantity'), type='quantity')
        # using position
        index_price = self.index_price(symbol)
        if action.get('side') == Action.long:
            position_side = 'LONG' if mode == 'open' else 'SHORT'
            # TODO: check
            if action.get('price') > index_price*(1+1e-2):
                return -2
        elif action.get('side') == Action.short:
            position_side = 'SHORT' if mode == 'open' else 'LONG'
            if action.get('price') < index_price*(1-1e-2):
                return -2

        # verify optimal state
        state = redisdb.standalone_operator('hget', symbol+redis_optimal_state, 'temp_stop_liquid')
        if int(state) != 0:
            print(f'temp_stop_liquid, Wait for {int(state)}s to recover')
            time.sleep(1)
            return -3

        if method == 'api':
            t0 = time.time()
            res = self.xt.send_order(symbol=symbol, price=action.get('price'), amount=action.get('quantity'), \
                order_side=action.get('side').value, order_type="LIMIT", position_side=position_side, timeInForce=timeInForce)
            t1 = time.time()
            print('SEND', action, res, t1-t0)
        elif method == 'sf':
            data = {
                "xtfuture": {
                    '_extra':{
                        "app_name": "test",
                        "user_name": "zhangsan",
                        "note": "---",
                    },
                    'accesskey': 'b1000aa0-9119-4ce7-a963-e7a0fe0055cb',
                    'secretkey': 'b9992066608e6b84b531cbf1bf36b06e9a79add8',
                    'market': symbol,
                    'price': action.get('price'),
                    'amount': action.get('quantity'),
                    'order_side': action.get('side').value,
                    'order_type': "LIMIT",
                    'position_side': position_side,
                    'timeInForce': timeInForce
                },
                "uuid": get_uuid(),
                'timeout': timeout,
            }
            data = json.dumps(data)
            self.event_data.data = data
            e = Event(XTFUTURE_SEND_ORDER_EVENT, self.event_data)
            self.engine.send(e)
            # print('send', action)
            # TODO: e.event_data.result return many result
            # res = e.event_data.result
            # print('send', res)            
        else:
            raise NotImplementedError
        return res

    def cancel(self, symbol, action, method='api'):
        '''
        params
            action: {'type', 'price', 'quantity', 'side', 'id'}
        '''
        def send(order):
            data = {
                "xtfuture": {
                    '_extra':{
                        "app_name": "test",
                        "user_name": "zhangsan",
                        "note": "---",
                    },
                    "data": order,
                    'accesskey': 'b1000aa0-9119-4ce7-a963-e7a0fe0055cb',
                    'secretkey': 'b9992066608e6b84b531cbf1bf36b06e9a79add8'},
                "uuid": get_uuid(),
                'timeout': timeout,
            }
            data = json.dumps(data)
            event_data = EventData()
            event_data.data = data
            e = Event(XTFUTURE_CANCEL_ORDER_EVENT, event_data)
            self.engine.send(e)
            # print('cancel', action)
            # TODO: e.event_data.result return many result
            # print('cancel', e.event_data.result)

        if not action:
            return False
        if method == 'api':
            if isinstance(action, list):
                for act in action:
                    res = self.xt.cancel_order(order_id=act.get('id'))    
            else:
                res = self.xt.cancel_order(order_id=action.get('id'))
            # print('CANCEL', action, res)
        elif method == 'sf':
            if isinstance(action, list):
                # order = [{"id": act.get('id'), "market": symbol} for act in action]
                for act in action:
                    send({"id": act.get('id'), "market": symbol})
            else:
                order = [{"id": action.get('id'), "market": symbol}]
                send(order)
        else:
            raise NotImplementedError
        return True


class BINANCEAPI(object):
    
    def __init__(self) -> None:
        self.pub_key = ''
        self.sec_key = ''
        self.__client = None
        self.__trade_price = 0
        self.__trade_time = int(time.time())
        self.proxies = {
            # 'https': '127.0.0.1:1095',
            # 'http': '127.0.0.1:1095'
        }

    @property
    def client(self):
        if self.__client is None:
            self.__client = Spot(key=self.pub_key, secret=self.sec_key, proxies=self.proxies)
        return self.__client

    def trades(self, symbol, increment=True):
        data = self.client.trades(symbol=symbol)
        data = pd.DataFrame(data).astype({'price': float, 'qty': float, 'time': int}).sort_values('time')
        data = data[data.time > self.__trade_time]
        if data.empty:
            return None
        price = data.iloc[-1].price
        qty = data.qty.sum() * price
        side = Action.long if price > self.__trade_price else Action.short
        action = {'type': 'send', 'price': price, 'quantity': qty, 'side': side, 'id': 0}
        if increment:
            self.__trade_time = data.time.max()
        return action

    def kline(self, symbol):
        data = self.client.klines(symbol=symbol, interval='1m')
        columns = ['open_time', 'open_price', 'high_price', 'low_price', 'close_price', 'volume', 'close_time', \
            'quote_asset_volume', 'number', 'taker_base_asset_volume', 'taker_quote_asset_volume', 'ignore']
        data = pd.DataFrame(data, columns=columns)
        data = data.astype(float)
        return data


xt_api = XTAPI()
binance_api = BINANCEAPI()
